Skip to content

feat: migrate v1 state into orchestrator v2#4400

Draft
juliusmarminge wants to merge 2 commits into
t3code/codex-turn-mappingfrom
codex/v1-v2-state-migration
Draft

feat: migrate v1 state into orchestrator v2#4400
juliusmarminge wants to merge 2 commits into
t3code/codex-turn-mappingfrom
codex/v1-v2-state-migration

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

  • converge packaged server, desktop, and SSH runtime state paths back to the production userdata directory
  • import v1 materialized thread shells synchronously, then hydrate complete transcripts lazily and in bounded background batches
  • hand imported visible history to the first v2 provider turn without trying to migrate v1 provider sessions or checkpoints
  • add migration 42 to track idempotent shell and transcript import progress

Migration behavior

  • startup work is proportional to thread shells plus at most two preview messages per thread; it does not replay the raw v1 event log
  • full transcripts are read from projection_thread_messages after command readiness and on first open/dispatch
  • deterministic IDs and a migration manifest make imports idempotent and retryable after interruption
  • migrated history supports normal viewing and continuation; v1 runs, checkpoints, forks, and tool activity are intentionally not reconstructed, while newly created v2 runs keep the full v2 feature set

Verification

  • 83 focused tests across 14 changed-path test files, plus a final 19-test service regression pass after restacking
  • targeted typechecks for server, contracts, desktop, and SSH packages
  • targeted lint, format, and diff checks
  • isolated browser verification that a seeded v1 thread is imported, rendered, and remains interactive

Stacked on #2829. The released migration-order repair now lives in the parent PR.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 184ffa61-8637-4241-8abc-3f15dd7a2e55

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/v1-v2-state-migration

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 23, 2026
Comment on lines +182 to +184
const suffix =
section.length <= remaining ? section : section.slice(section.length - remaining);
selected.unshift(suffix);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium orchestration-v2/ContextHandoffService.ts:182

makeLegacyImportSummary truncates from the left when a section exceeds the remaining character budget, taking a raw suffix via section.slice(section.length - remaining). When this cut lands inside the first retained section, the User: or Assistant: label is dropped and the text can start mid-word, so the imported context begins with unattributed, fragmentary text. Consider preserving the role prefix and truncating only the message body, or omitting the partial section entirely.

Suggested change
const suffix =
section.length <= remaining ? section : section.slice(section.length - remaining);
selected.unshift(suffix);
const suffix =
section.length <= remaining
? section
: section.slice(section.length - remaining);
selected.unshift(suffix);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/ContextHandoffService.ts around lines 182-184:

`makeLegacyImportSummary` truncates from the left when a section exceeds the remaining character budget, taking a raw suffix via `section.slice(section.length - remaining)`. When this cut lands inside the first retained section, the `User:` or `Assistant:` label is dropped and the text can start mid-word, so the imported context begins with unattributed, fragmentary text. Consider preserving the role prefix and truncating only the message body, or omitting the partial section entirely.

const getThreadSnapshot: ThreadManagementServiceShape["getThreadSnapshot"] = (threadId) =>
ensureLegacyTranscript(threadId).pipe(Effect.andThen(orchestrator.getThreadSnapshot(threadId)));

const dispatch: ThreadManagementServiceShape["dispatch"] = (command) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High orchestration-v2/ThreadManagementService.ts:261

The dispatch wrapper only calls ensureLegacyTranscript for message.dispatch, thread.fork, and thread.merge_back. All other commands targeting an existing migrated thread — such as thread.metadata.update, thread.archive, or thread.delete — bypass hydration and dispatch directly. When a later projection read triggers the importer, its transcript marker emits a thread.metadata-updated event from the stale v1 row, overwriting the newer title/archive/deletion metadata. Hydrate every command that operates on an existing thread before dispatching, not just those three types.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/ThreadManagementService.ts around line 261:

The `dispatch` wrapper only calls `ensureLegacyTranscript` for `message.dispatch`, `thread.fork`, and `thread.merge_back`. All other commands targeting an existing migrated thread — such as `thread.metadata.update`, `thread.archive`, or `thread.delete` — bypass hydration and dispatch directly. When a later projection read triggers the importer, its transcript marker emits a `thread.metadata-updated` event from the stale v1 row, overwriting the newer title/archive/deletion metadata. Hydrate every command that operates on an existing thread before dispatching, not just those three types.

Comment on lines +242 to +251
const ensureLegacyTranscript = (threadId: ThreadId) =>
legacyImporter.ensureTranscript(threadId).pipe(
Effect.tapError((cause) =>
Effect.logWarning("Unable to hydrate migrated v1 thread transcript", {
threadId,
cause,
}),
),
Effect.ignore,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium orchestration-v2/ThreadManagementService.ts:242

ensureLegacyTranscript swallows all hydration failures with Effect.ignore, so every call site (dispatch, getThreadProjection, getThreadSnapshot) proceeds as if the legacy transcript was loaded even when it wasn't. For message.dispatch this means the provider turn is built from only the shell preview instead of the full legacy conversation — context omitted from that turn cannot be restored by a later retry. Consider letting hydration failures propagate (or retrying) so dispatch does not silently continue with an incomplete transcript.

-  const ensureLegacyTranscript = (threadId: ThreadId) =>
-    legacyImporter.ensureTranscript(threadId).pipe(
-      Effect.tapError((cause) =>
-        Effect.logWarning("Unable to hydrate migrated v1 thread transcript", {
-          threadId,
-          cause,
-        }),
-      ),
-      Effect.ignore,
-    );
+  const ensureLegacyTranscript = (threadId: ThreadId) =>
+    legacyImporter.ensureTranscript(threadId).pipe(
+      Effect.tapError((cause) =>
+        Effect.logWarning("Unable to hydrate migrated v1 thread transcript", {
+          threadId,
+          cause,
+        }),
+      ),
+    );
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/ThreadManagementService.ts around lines 242-251:

`ensureLegacyTranscript` swallows all hydration failures with `Effect.ignore`, so every call site (`dispatch`, `getThreadProjection`, `getThreadSnapshot`) proceeds as if the legacy transcript was loaded even when it wasn't. For `message.dispatch` this means the provider turn is built from only the shell preview instead of the full legacy conversation — context omitted from that turn cannot be restored by a later retry. Consider letting hydration failures propagate (or retrying) so dispatch does not silently continue with an incomplete transcript.

@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch from 0fff2d0 to 1d15173 Compare July 24, 2026 10:12
lineage: projection.thread.lineage,
forkedFrom: projection.thread.forkedFrom,
activeProviderThreadId: projection.thread.activeProviderThreadId,
...(projection.thread.historyOrigin === undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium orchestration-v2/ProjectionStore.ts:769

Production shell snapshots silently drop historyOrigin from imported threads, while in-memory/test snapshots include it. threadShellFromProjection (used by the in-memory store) now propagates historyOrigin as an optional field, but the production shellFromState constructor still omits input.state.thread.historyOrigin, so the field is lost for all production shell snapshots. Add the same optional-field propagation to shellFromState.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/ProjectionStore.ts around line 769:

Production shell snapshots silently drop `historyOrigin` from imported threads, while in-memory/test snapshots include it. `threadShellFromProjection` (used by the in-memory store) now propagates `historyOrigin` as an optional field, but the production `shellFromState` constructor still omits `input.state.thread.historyOrigin`, so the field is lost for all production shell snapshots. Add the same optional-field propagation to `shellFromState`.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Jul 24, 2026
);
}

export const ChatHeader = memo(function ChatHeader({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium chat/ChatHeader.tsx:11

Scripts declared in a project's checked-in t3.json are no longer shown anywhere in the UI. The old ChatHeader called useT3ProjectFileScripts and passed the result as fileScripts to ProjectScriptsControl; after this PR removes that call site, fileScripts is never populated, so ProjectScriptsControl only sees the default empty array and the file-based scripts disappear from the picker. Wire useT3ProjectFileScripts back into wherever ProjectScriptsControl is now rendered (e.g., ThreadDetailsPanel) and pass its result as fileScripts.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatHeader.tsx around line 11:

Scripts declared in a project's checked-in `t3.json` are no longer shown anywhere in the UI. The old `ChatHeader` called `useT3ProjectFileScripts` and passed the result as `fileScripts` to `ProjectScriptsControl`; after this PR removes that call site, `fileScripts` is never populated, so `ProjectScriptsControl` only sees the default empty array and the file-based scripts disappear from the picker. Wire `useT3ProjectFileScripts` back into wherever `ProjectScriptsControl` is now rendered (e.g., `ThreadDetailsPanel`) and pass its result as `fileScripts`.

@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch from a5b5085 to 952df09 Compare July 24, 2026 11:53
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Jul 24, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch 3 times, most recently from 4f338ff to 4e8f03a Compare July 24, 2026 12:31
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 6b1c011 to cd04ad1 Compare July 24, 2026 13:09
@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch from 4e8f03a to c827eba Compare July 24, 2026 13:09
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from cd04ad1 to 1e58e65 Compare July 24, 2026 13:31
@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch from c827eba to 1842ade Compare July 24, 2026 13:31
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 1e58e65 to a286c60 Compare July 24, 2026 13:37
@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch from 1842ade to f9f39db Compare July 24, 2026 13:38
@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch 2 times, most recently from 25e9e1b to f845ccf Compare July 24, 2026 15:49
juliusmarminge and others added 2 commits July 24, 2026 18:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarminge force-pushed the codex/v1-v2-state-migration branch from f845ccf to 8008280 Compare July 24, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant